/**
 *  Platypus: Page Layout and Typesetting Software (free at platypus.pz.org)
 *
 *  Platypus is (c) Copyright 2012 Pacific Data Works LLC. All Rights Reserved.
 *  Licensed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html)
 */
package org.pz.platypus.parser;


import com.google.common.annotations.VisibleForTesting;

import org.pz.platypus.*;
import org.pz.platypus.interfaces.*;
import org.pz.platypus.utilities.TextTransforms;


import static com.google.common.base.Preconditions.*;

/**
 * @author alb
 */
public class LineParser
{
    private Source source;
    private int fileNum;
    private int lineNum;
    private String content;
    protected TokenList tokens;
    private boolean inCode = false;
    private boolean inBlockComment;
    private boolean inScript;
    private GDD gdd;
    private ConfigFile configFile;

    public TokenList parseLine( final InputLine line, ParserState parserState, final GDD Gdd)
    {
        gdd = checkNotNull( Gdd );
        configFile = checkNotNull( gdd.configFile );
        initialize( line, parserState );
        parseSegments( parserState );
        return( tokens );
    }

    @VisibleForTesting
    public void parseSegments( ParserState state )
    {
        // is it a blank line?
        Character c = content.charAt(0);
        if(( c == '\n' || c == '\r' ) && ! inBlockComment && ! inScript ) {
            tokens.add( new Token( source, TokenType.COMMAND, "[CR]", "[CR]", null ));
            return;
        }

        // roll through the segments of the line (segments = parsable units: text, command, comment).
        // every line ends with '\n', which is the last parsable token in the line.
        int segmentStartPoint = 0;
        while( segmentStartPoint < content.length() )
        {
            ParseContext context = new ParseContext( gdd, source, content, segmentStartPoint );

            if ( context.atEndOfLine() ) {
                emitEolToken( source );
                break;
            }

            // are we in a block comment?
            if ( inBlockComment ) {
                int eoCommentBlockLocation = content.indexOf( state.closingCommentBlock, segmentStartPoint );
                if( eoCommentBlockLocation == -1 ) {  // in a block comment that is NOT closed on this line.
                    if( includeCommentsInTokenStream() ) {  // if comments are passed through, emit the tokens
                        emitCommentToken(new Token(source, TokenType.BLOCK_COMMENT,
                                TextTransforms.truncate(content, 1)));
                        emitEolToken( source );
                        return;
                    }
                    return; // if comments are not passed through, don't emit anything for this line.
                }
                else {                              // in a block comment and closing symbol is found.
                    int amtToSkip =
                            handleClosingBlockComment( context, eoCommentBlockLocation, state.closingCommentBlock );
                    state.closingCommentBlock = null;
                    inBlockComment = false;
                    segmentStartPoint += amtToSkip;
                    continue;                       // keep parsing remaining tokens
                }
            }

            segmentStartPoint = parseSegment( context, state );
//            if ( segmentStartPoint == Status.UNFIXABLE_PARSE_ERR ||
//                    segmentStartPoint == Status.INVALID_PARAM ) {
//                return( Status.UNFIXABLE_PARSE_ERR );
//            }
        }
    }

    /**
     * Parse a line of the input file
     * @param line line to be parsed
     * @param newTokens the TokenList to which we're adding tokens
     * @return Status.OK or Status.UNFIXABLE_PARSE_ERR, which is a serious, stop-the-Platypus error
     */
//    public int parseLine( final TokenList newTokens, final InputLine line )
//    {
//        if ( newTokens == null || line == null ) {
//            return( Status.INVALID_PARAM_NULL );
//        }
//
//        if( inCode ) {} // process being in code and return. //TODO
//
//        // a line comment?
//        if( LineCommentParser.isLineComment( line.getContent(), blockCommentClosingSymbol )) {
//            addToken( newTokens, TokenType.LINE_COMMENT, line.getContent(), line.getSource() );
//            return( Status.OK );
//        }
//
//        // is it a blank line?
//        Character c = line.getContent().charAt(0);
//        if( c == '\n' || c == '\r' ) {
//            newTokens.add( new Token( line.getSource(), TokenType.COMMAND, "[CR]", "[CR]",
//                    null ));
//            return( Status.OK );
//        }
//
//        // isLineComment segments of the line (segments = parsable units: text, command, comment).
//        // '\n' is the last parsable token in the line.
//        int segmentStartPoint = 0;
//        String content = line.getContent();
//        while( segmentStartPoint < content.length() )
//        {
//
//            ParseContext context = new ParseContext( gdd, line.getSource(), content, segmentStartPoint);
//
//            if ( context.atEndOfLine() ) {
//                emitEolToken( newTokens, context.source );
//                break;
//            }
//
//            // are we in a block comment?
//            if ( blockCommentClosingSymbol != null ) {
//                int closeCommentStart = content.indexOf( blockCommentClosingSymbol, segmentStartPoint );
//                if( closeCommentStart == -1 ) {
//                    addToken( newTokens, TokenType.BLOCK_COMMENT, content, context.source );
//                    return( Status.OK );
//                }
//                else {
//                    int i = handleClosingBlockComment( newTokens, context, closeCommentStart );
//                    blockCommentClosingSymbol = null;
//                    segmentStartPoint += i;
//                    continue;
//                }
//            }
//
//            segmentStartPoint = parseSegment( context, newTokens );
//            if ( segmentStartPoint == Status.UNFIXABLE_PARSE_ERR ||
//                    segmentStartPoint == Status.INVALID_PARAM_NULL ||
//                    segmentStartPoint == Status.INVALID_PARAM ) {
//                return( Status.UNFIXABLE_PARSE_ERR );
//            }
//
//            // if there is code to inject (generally, as the result of a macro
//            // expansion, then remove the amount of text occupied by the macro
//            // and replace it with the macro body. Then restart the isLineComment point
//            // at the beginning of this new input string.
//            if ( gdd.getExpandedMacro() != null ) {
//                content = gdd.getExpandedMacro() + content.substring( segmentStartPoint );
//                segmentStartPoint = 0;
//                gdd.setExpandedMacro( null );
//                continue;
//            }
//        }
//        return( Status.OK );
//    }

    /**
     * The primary line-parsing routine. It finds one segment (text, a command, a comment block)
     * parses it, adds it to tokens, and returns after moving parsePoint to the first character
     * of the next segment. This method is called repeatedly by parseLine() until parsePoint
     * points to the '\n' that ends the input line.
     *
     * Note: assumes that all segments end with a \n
     *
     * @param parseContext the source context data
     * @return the new isLineComment point or, if an error that can't be fixed in the parser
     *         occurs, returns Status.UNFIXABLE_PARSE_ERR
     */
    public int parseSegment( final ParseContext parseContext, final ParserState pState )
    {
        ParseContext context = checkNotNull( parseContext );

        int parsePoint = context.startPoint;
        String contentToParse = parseContext.getContent().substring( parsePoint );

        while( true )
        {
            // if we're at a \n, write out all the preceding text we've seen, and exit.
            // On the next loop through in parseLine(), the \n will be recognized and
            // be processed as part of end of line.

            if ( context.isEnd( parsePoint )) {
                if( ! inScript ) {
                    emitTextToken( context, parsePoint );
                    return( parsePoint );
                }
            }

            // if in a script...
            if( inScript ) {
                return new ScriptSegmentTokenGen().gen( parsePoint, contentToParse, tokens, source, pState );
            }

            if ( context.isCommandStart( parsePoint ) ) {
                if ( CommandStartParser.isItACommand( context.chars, parsePoint )) {
                    // if the [ is not the beginning of the segment, then
                    // write out all that precedes it as text. Then loop.
                    // the new segment starting with [ will come back
                    // here and proceed with processing the command
                    if ( context.startPoint != parsePoint ) {
                        emitTextToken( context, parsePoint-1 );
                        return( parsePoint );
                    }

                    // is it a macro?
                    if( context.chars[parsePoint+1] == '$') {
                        return( processMacro( context, tokens ) + 1 );
                    }  // TODO: might be a need to verify a macro does not contain a block comment end.

                    // is it a block comment?
                    if( context.chars[parsePoint+1] == '%' ) {
                        return( processBlockComment( context, tokens ));
                    }

                    // it's a command
                    parsePoint = processCommand( context, tokens );
                    return( parsePoint );
                }
                else   // it's not a command.
                {
                    // if it's an escaped [ (so: /[ ), we need to check whether we passthrough
                    // the escape char. If not (the general case), we write all the text up to
                    // the escape char, plus the [ after it to a text token.
                    if( CommandStartParser.isItEscapedCommandStart( context.chars, parsePoint )) {
                        if( ! includeCommandEscCharInTokenStream() ) {
                            if( parsePoint - context.startPoint > 2 ) {// there's preceding text
                                emitTextToken( context, parsePoint-2 );
                            }
                            tokens.add( new Token( context.source, TokenType.TEXT, "[" ));
                            return( ++parsePoint );
                        }
                    }
                }
            }

            // if a character is not a command, comment, macro or a LF, it's text; so keep looping.
            parsePoint++;
        }
    }

    /**
     * Write the closed block comment to the token list and return the amount by which
     * the isLineComment point needs to be moved up to get past the block comment.
     * @param context the parsing context info
     * @param closeCommentStart where the beginning of the close to the block comment is
     * @param closingCommentSymbol the string that marks the end of this block comment
     * @return number of chars to move forward to get past the closing of the block comment
     */
    @VisibleForTesting
    public int handleClosingBlockComment( final ParseContext context,
                                          final int closeCommentStart, final String closingCommentSymbol )
    {
        int amtToSkip = closeCommentStart + closingCommentSymbol.length();
        emitCommentToken( new Token( context.source, TokenType.BLOCK_COMMENT, context.segment( amtToSkip )));

        // if the closing block-comment symbol occurred after a %% line comment marker, then the rest
        // of the line should be considered a line comment.
        if( closeCommentStart != 0 && context.getContent().startsWith( "%%" )) {
            emitCommentToken( new Token( context.source, TokenType.LINE_COMMENT,
                    context.getContent().substring( closeCommentStart + closingCommentSymbol.length() )));
            amtToSkip = context.getContent().length() - closeCommentStart;
        }
        return( amtToSkip );
    }

    public boolean includeCommentsInTokenStream()
    {
        String keepComments =
                configFile.getConfigItem( "pi.out." + gdd.sysStrings.get( "_FORMAT" ) + ".keep_comments" );
        return ( keepComments.toLowerCase().equals( "yes" ));
    }

    public boolean includeCommandEscCharInTokenStream()
    {
        String keepEscChar = configFile.getConfigItem(
                                "pi.out." + gdd.sysStrings.get( "_FORMAT" ) + ".passthrough_escape_char" );
        return( keepEscChar.toLowerCase().equals( "yes" ));
    }
    /**
     * Emits the comment token if comments are supposed to written out to the token stream (fairly rare).
     * @param token token to emit
     */
    protected void emitCommentToken( final Token token )
    {
        if( includeCommentsInTokenStream())
            tokens.add( checkNotNull( token ));
    }

    /**
     * Emits the so-called "soft" EOL command used when at end of line.
     * @param source file# and line# of token
     */
    protected void emitEolToken( final Source source )
    {
        TokenType prevTokenType;
        Token prevToken = tokens.getLastToken();

        // if the previous token was a line comment or a block comment, don't emit the [cr] unless
        // the config file specifies keep_comments=yes for this output plugin.
        if( prevToken != null ) {
            prevTokenType = prevToken.getType();
            if( prevTokenType == TokenType.LINE_COMMENT || prevTokenType == TokenType.BLOCK_COMMENT ) {
                String keepComments =
                    configFile.getConfigItem( "pi.out." + gdd.sysStrings.get( "_FORMAT" ) + ".keep_comments" );
                if( ! keepComments.toLowerCase().equals( "yes" ))
                    return;
            }
        }

        tokens.add( new Token( source, TokenType.COMMAND, "[cr]", "[cr]", null ));
    }

    protected void emitTextToken( final ParseContext context, int endPoint )
    {
        emitTextToken( context.startPoint, endPoint, context.chars, context.source );
    }

    /**
     * Write out a set of text characters to the token list as a text token
     * @param start subscript of first char in text
     * @param end subscript of last char in text
     * @param text array of chars containing the line from which the text is extracted
     * @param source the file# and line#
     * @return  right now, always Status.OK. If this doesn't change, might change to void.
     */
    protected void emitTextToken( final int start, final int end, final char[] text, final Source source )
    {
        checkArgument( start < 0 && start < end );
        checkArgument( end > 0 );
        checkNotNull( source );
        checkNotNull(text);

        final String tokenText = TextTransforms.charArrayToString(text, start, end);

        final Token textToken = new Token( source, TokenType.TEXT, tokenText );
        tokens.add( textToken );
    }

    @VisibleForTesting
    public void initialize( final InputLine line, final ParserState parserState )
    {
        source = line.getSource();
        fileNum = checkNotNull( line ).getSource().getFileNumber();
        lineNum = line.getSource().getLineNumber();
        content = checkNotNull(line.getContent());
        inBlockComment = parserState.closingCommentBlock != null;
        inScript = parserState.inScript;
        tokens = new TokenList();
    }

    //-------------------------

    /**
     * Processes block comments. It identifies the starting marker, computes the ending marker,
     * and verifies checks whether the closing marker is on the same line. If so, it handles the
     * block comment. Otherwise, it stores the closing marker in blockCommentClosingSymbol.
     *
     * @param tokens the token list to add the block-comment token to
     * @param context the parser context/location info.
     * @return the new isLineComment point //TODO: put this in a block-comment parser?
     */
    public int processBlockComment( final ParseContext context, TokenList tokens )
    {
//        assert context != null;
//        assert tokens  != null;
//
//        BlockCommentParser bcp = new BlockCommentParser();
//
//        blockCommentClosingSymbol = bcp.computeClosingMarker( context.chars, context.startPoint );
//        if( blockCommentClosingSymbol == null ) {
//            //curr: now what?!? throw exception (used only by tests)
//        }
//
//        // if it's a multiline block comment, write it out and point to EOL.
//        int endPoint = context.chars.length - 1;
//
//        // but first check to see whether block closes on this same line.
//        if ( blockClosesOnSameLine( context, blockCommentClosingSymbol )) {
//            endPoint = context.getLocation( blockCommentClosingSymbol ) +
//                    blockCommentClosingSymbol.length();
//            blockCommentClosingSymbol = null;
//        }
//
//        addToken( tokens, TokenType.BLOCK_COMMENT, context.segment( endPoint ), context.source );
//        return( endPoint );
        return( 19829183 ); //delete. just to shut up errors
    }

    private boolean blockClosesOnSameLine( final ParseContext context,
                                           final String blockCommentClosingSymbol )
    {
        return context.containsInRemainingChars( blockCommentClosingSymbol );
    }

    /**
     * Just grabs the command and writes it to the token list
     *
     * @param context parse context: sourcefile# & line#, parsePoint, line as string, line as char[]
     * @param tokens token list to which we will add the tokens derived from the command
     * @return the updated version of the isLineComment point.
     */
    public int processCommand( final ParseContext context, TokenList tokens)
    {
        final String commandRoot;

        try {
            commandRoot = new CommandRootExtractor( gdd ).getRoot( context );
        }
        catch ( IndexOutOfBoundsException iobe ) {  // error warning to user is done in CommandRootExtractor class.
            return( context.getContent().length() - context.startPoint ); //jump to the end of the line
        }

        ICommand command = gdd.commandTable.getCommand( commandRoot );
        if ( command != null ) {                                   //Todo: figure out what to do w/ inCode
            return( command.process( gdd, context, tokens, inCode ) + context.startPoint );
        }
        else {
            // the command root is not found, so it's probably text that looks like command
            // so, write out the root as text. The rest of the command will be treated as
            // text on the next pass through the rest of the segment.
            invalidCommandError( context, tokens, commandRoot );
            return( context.startPoint + commandRoot.length() );
        }
    }

    /**
     * Write an error message to console saying that the command-like token is not actually
     * a Platypus command, and then write the token out to the output as a text token.
     *
     * @param context the parser context
     * @param tokens list of tokens to which this token will be added
     * @param commandRoot the root of the command, were it a real command.
     */
    void invalidCommandError( final ParseContext context,
                              final TokenList tokens, final String commandRoot )
    {
        gdd.log.info( gdd.getLit( "FILE#" ) + " " + context.source.getFileNumber() + " " +
                      gdd.getLit( "LINE#" ) + " " + context.source.getLineNumber() + " " +
                      commandRoot + " " + gdd.getLit( "ERROR.NOT_PALYTPUS_COMMAND" ));
        emitTextToken( context, context.startPoint + commandRoot.length() - 1 );
    }

    /**
     * If the config file for this output format says that Platypus expands macros, this routine
     * looks up the macro, expands it, and puts the expanded form in gdd.expandedMacro. If the
     * config file says not to process macros, the macro is simply written out to the token stream.
     *
     * @param tokens list of tokens we're building up in parser. (Used only if macros not processed)
     * @param context the parsing context info
     * @return the new isLineComment point after the macro, or Status.UNFIXABLE_PARSE_ERR if an error occurs
     */
    public int processMacro( final ParseContext context, final TokenList tokens )
    {
        MacroParser mp = new MacroParser( gdd );
        return( mp.parse( context, tokens ));
    }
}